package base

import (
	"context"
	"encoding/json"
	jsoniter "github.com/json-iterator/go"
	"github.com/zeromicro/go-zero/core/logc"
	"github.com/zeromicro/go-zero/core/logx"
	"github.com/zeromicro/go-zero/zrpc"
	"google.golang.org/grpc"
	"google.golang.org/grpc/metadata"
	"payme3000/framework/config_center"
	"payme3000/framework/db"
	"payme3000/framework/errorx"
	"payme3000/framework/util/check"
	"payme3000/framework/util/math"
	"strings"
	"time"
)

type (
	// IDispatcher RPC调度模块
	IDispatcher interface {
		IModule
		ISetAppConf
		ISetConfigCenter
		placeholderDispatcher()
	}

	// Dispatcher IDispatcher的基础实现
	Dispatcher struct {
		Module

		AppConf      *AppConf                    // 启动配置, 依赖项
		ConfigCenter *config_center.ConfigCenter // 配置中心
	}

	// ReTryDispatch 提供重试功能的调用
	ReTryDispatch[REQ any, RSP any] struct {
		Handle func(context.Context, REQ, ...grpc.CallOption) (RSP, error) // 调用回调
		Check  func(RSP, error) bool                                       // 重试判定函数回调, 返回false时触发重试
		Times  int8                                                        // 最大重试次数
	}
)

// Conn 返回grpc conn
func (d *Dispatcher) Conn(conf *zrpc.RpcClientConf) (grpc.ClientConnInterface, error) {
	// 必须提供服务发现的key
	if conf == nil || conf.Etcd.Key == "" {
		return nil, errorx.Errorf(context.Background(), "etcd key not found")
	}

	// 设置默认值 todo 支持服务的dispatcher可自定义
	conf.NonBlock = true
	conf.Timeout = d.AppConf.DefaultRequestTimeout
	conf.Middlewares.Trace = true
	conf.Middlewares.Duration = true
	conf.Middlewares.Prometheus = true
	conf.Middlewares.Breaker = true
	conf.Middlewares.Timeout = true

	// 如果没提供etcd的hosts, 尝试从ConfigCenter获取
	if len(conf.Etcd.Hosts) == 0 {
		if dbEtcdConf, err := d.ConfigCenter.Get("etcd_discovery"); err != nil {
			logx.Errorf("get etcd_discovery error")
			return nil, err
		} else if dbEtcdConf == nil {
			return nil, errorx.Throw(context.Background(), "etcd_discovery not found")
		} else {
			etcdConf := &db.Config{}
			if err := jsoniter.Unmarshal([]byte(*dbEtcdConf), etcdConf); err != nil {
				return nil, errorx.Throw(context.Background(), err)
			}

			hosts := strings.Split(etcdConf.Host, ",")
			if len(hosts) == 0 {
				return nil, errorx.Errorf(context.Background(), "illegal etcd host with etcdConf:%+v", etcdConf)
			}

			conf.Etcd.Hosts = hosts

			if etcdConf.Username != "" {
				conf.Etcd.User = etcdConf.Username
			}

			if etcdConf.Password != "" {
				conf.Etcd.Pass = etcdConf.Password
			}
		}
	}

	//  创建zrpc client
	client, err := zrpc.NewClient(
		*conf,
		zrpc.WithUnaryClientInterceptor(UnaryClientTraceInterceptor),
	)
	if err != nil {
		return nil, errorx.Throw(context.Background(), err)
	}

	return client.Conn(), nil
}

func (d *Dispatcher) SetAppConf(appConf *AppConf) {
	d.AppConf = appConf
}

func (d *Dispatcher) SetConfigCenter(configCenter *config_center.ConfigCenter) {
	d.ConfigCenter = configCenter
}

func (d *Dispatcher) placeholderDispatcher() {}

func NewReTryDispatch[REQ any, RSP any](
	handle func(context.Context, REQ, ...grpc.CallOption) (RSP, error),
) *ReTryDispatch[REQ, RSP] {
	return &ReTryDispatch[REQ, RSP]{
		Handle: handle,
		Times:  3,
	}
}

func (t *ReTryDispatch[REQ, RSP]) WithCheck(check func(RSP, error) bool) *ReTryDispatch[REQ, RSP] {
	t.Check = check
	return t
}

func (t *ReTryDispatch[REQ, RSP]) WithTimes(times int8) *ReTryDispatch[REQ, RSP] {
	t.Times = times
	return t
}

func (t *ReTryDispatch[REQ, RSP]) Dispatch(ctx context.Context, req REQ, opts ...grpc.CallOption) (RSP, error) {
	i := t.Times
	for {
		i--
		rsp, err := t.Handle(ctx, req, opts...)
		// 没提供自定义Check就用默认判断
		// 默认判断返回true || 自定义判断返回true || 次数用尽, 返回结果
		if (t.Check == nil && err == nil && !check.IsRealNil(rsp)) ||
			(t.Check != nil && t.Check(rsp, err)) ||
			i <= 0 {
			if err != nil {
				_ = errorx.Errorf(ctx, "dispatch error with err:%s, req:%+v, rsp:%+v", err, req, rsp)
			}

			return rsp, err
		}

		time.Sleep(time.Duration(math.Max(t.Times-i, 1)) * time.Second)
		logc.Infof(ctx, "dispatch retry with times:%d, req:%+v, rsp:%+v", i, req, rsp)
	}
}

func UnaryClientTraceInterceptor(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
	if uid := ctx.Value("uid"); uid != nil {
		if _uid, ok := uid.(json.Number); ok {
			ctx = metadata.AppendToOutgoingContext(ctx, "uid", string(_uid))
		}
	}

	return invoker(ctx, method, req, reply, cc, opts...)
}
